Skip to content

fix: don't abort on worker termination during module load - #1993

Open
edusperoni wants to merge 1 commit into
feat/v8-14from
fix/worker-terminate-crashes
Open

fix: don't abort on worker termination during module load#1993
edusperoni wants to merge 1 commit into
feat/v8-14from
fix/worker-terminate-crashes

Conversation

@edusperoni

Copy link
Copy Markdown
Collaborator

Description

Targets feat/v8-14 (#1987) so it rides the upcoming merge; the bugs themselves are long-standing and exist on main unchanged (verified — every fixed site is byte-identical there).

Intermittent process aborts/crashes (~20% of cold full-suite runs on an emulator, always in the worker-heavy early phase) were root-caused to worker.terminate() landing while the worker is still loading its script. Once TerminateExecution() is armed, every V8 entry that runs JS returns an empty MaybeLocal — and three call sites on exactly this path unwrapped without checking. The proven signature (symbolized from a device tombstone) is Fatal error in v8::ToLocalChecked / Empty MaybeLocal on a worker thread, aborting from ModuleInternal::LoadModule.

Fixes

  • ModuleInternal.cppscript->Run() was unwrapped with ToLocalChecked() one line before the tc.HasCaught() guard meant to handle it; now checked first (matching the sibling compile sites in the same function). Same treatment for the unconditional __extends global lookup a few lines down.
  • NativeScriptException.cpp — the TryCatch constructor now bails out early on tc.HasTerminated() || tc.Message().IsEmpty() with a fallback message, and no longer builds a Persistent from an empty exception handle (which left ReThrowToV8 dereferencing an empty Local).
  • CallbackHandlers.cppCallWorkerScopeOnErrorHandle unwrapped the global onerror lookup with ToLocalChecked(); it runs precisely when a worker script fails to load. Now ToLocal() + early return.
  • Runtime.cppstruct sigaction was never zero-initialized, leaving sa_mask/sa_flags as stack garbage for both signal handlers (a source of run-to-run nondeterminism in crash behavior).

No behavior change for healthy paths: termination reporting was already correctly suppressed (isTerminating_ is set before TerminateExecution() and guarded in BackgroundLooper/CallWorkerScopeOnErrorHandle); no existing spec asserts an error for terminate-during-load.

Known remaining flake (out of scope): while re-verifying the reproduction on the unfixed build, one crash of a different signature was captured — a bionic Pointer tag ... was truncated SIGABRT on a worker thread right after isolate creation (heap corruption, not an empty-handle abort). It was not observed in 27 cold runs of the fixed build, but this PR does not claim to fix it; it's an open follow-up.

Related Pull Requests

Does your pull request have unit tests?

Yes — tests/testWorkerTerminateDuringLoad.js: the worker spins at module scope so terminate() deterministically lands inside the module-function call (the wide window; the script->Run() window is microseconds and can't be hit reliably), asserting no onerror fires and the process survives. It cannot fail spuriously. It lives in the app's tests/ because shared/Workers/ is a submodule shared with iOS. Suite: 606 specs, 0 failures (605 baseline + 1).

Statistical verification: unfixed build reproduces at ~20% per cold full-suite run (re-armed before fixing: 1/5); fixed build ran 27 cold full-suite runs with zero crashes and a clean dropbox/tombstone sweep.

worker.terminate() calls Isolate::TerminateExecution() on the worker
isolate from the parent thread, after which every V8 entry that runs JS
hands back an empty handle. Three call sites in the worker's script-load
path unwrapped those handles without checking:

- ModuleInternal::LoadModule called script->Run(...).ToLocalChecked() one
  line before the tc.HasCaught() guard meant to handle exactly that, so a
  terminate landing mid-load killed the process with "Fatal error in
  v8::ToLocalChecked / Empty MaybeLocal".
- The same function unwrapped the __extends lookup unconditionally.
- CallWorkerScopeOnErrorHandle, which runs precisely when a worker script
  fails to load, unwrapped the global "onerror" lookup.

NativeScriptException's TryCatch constructor then dereferenced
tc.Message() unconditionally. A terminated TryCatch exposes no message
object, so building the error to report turned the abort into a SIGSEGV,
which the runtime's own signal handler converted into an opaque "JNI
Exception occurred (SIGSEGV)" and no tombstone.

All four now test before unwrapping, matching the sibling compile sites
in LoadModule. Reporting already suppresses termination -- BackgroundLooper
guards on isTerminating_ and CallWorkerScopeOnErrorHandle returns early
for a terminating wrapper -- so a terminate during load unwinds as a
normal shutdown.

Also zero-initialises the sigaction struct used to install the SIGABRT and
SIGSEGV handlers, whose sa_mask and sa_flags were stack garbage.

The device suite hit this on roughly 20% of cold runs (pm clear + launch)
on an arm64 emulator, always in a worker spawned by the Workers suite
within the first seconds of the run; the faulting frame symbolised to
ModuleInternal::LoadModule. 27 cold runs on the fixed build are clean.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 28dac7dd-4b9c-4a00-97f2-e7b8cc6d2960

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant